
平常用 Kotlin 寫 Web API 時,Ktor 的寫法很自然。routing { get("/users/{id}") { ... } }、install(ContentNegotiation)、call.respond(...) 這些 API 看起來都很順,照著文件也能很快做出一個可以跑的服務
可是用久之後我會想,如果要自己做出類似 Ktor 的使用體驗,到底需要哪些東西
請求怎麼從 HTTP Server 進到框架 ? Route 是怎麼匹配的 ? {id} 這種路徑參數要放在哪裡 ? Middleware 為什麼可以一層包一層 ? Plugin 到底只是語法糖,還是真的改變了框架的擴充方式 ?
所以這次想用和 Kotlin Lambda 系列類似的練習方式,那個系列是手刻 Collection API 來理解 Lambda,這個系列則是手刻一個類似 Ktor 的輕量 Web 框架 Relix,藉由重做 Request、Response、Routing、Pipeline、Plugin、Content Negotiation 這些核心功能,重新理解 Kotlin DSL 和框架設計
說到底就是想把平常藏在 routing {}、install(...)、receive<T>() 背後的機制拆開來看
Relix 取自 relation、lightweight、extension 三個字,對應這個框架想做的三件事。把 Request、Router、Middleware、Plugin、Handler 之間的關係設計清楚,維持輕量核心,再用 Kotlin 的 extension、DSL 與 receiver lambda 逐步擴充能力
這個系列會用 32 篇文章,從 JDK 內建的 HttpServer 起步,一步一步疊出 Relix
Relix 的目的不是要複製 Ktor 的原始碼,也不是追求一模一樣的 API,而是挑出 Ktor 這類 Web 框架的主要能力,自己用 Kotlin 做一次,像 Router 這種看起來很單純的東西,我們也會把 404 / 405 / trailing slash / HEAD fallback 這些細節自己處理過一遍
主軸很單純,每次挑一個 Ktor 風格框架會需要的能力,先問為什麼需要,再用測試確認行為,然後寫最小實作,最後整理 API,功能會從小到大長出來,不會一開始就丟一整套框架架構
系列採用漸進式節奏,先同步 I/O 把核心概念講清楚,Pipeline 穩了再升級
suspend管線
大致會這樣走
| 部分 | 篇章 | 主題 |
|---|---|---|
| 導讀與環境 | 01-02 | 系列動機、開發環境、第一個能跑的測試 |
| HTTP 核心 | 03-06 | 啟動 HTTP Server,封裝 Request / Response / RelixCall 與 Application / Engine,做出 TestKit |
| 路由系統 | 07-11 | 從 Map 路由進化到支援路徑參數的 Routing DSL |
| Pipeline 與擴充 | 12-18 | 打造 Pipeline / Middleware 洋蔥模型,再包成 Plugin 系統 |
| 內容處理與 JSON | 19-21 | 處理 Request Body、接上 kotlinx.serialization、實作 receive |
| 驗證與安全 | 22-23 | 輸入驗證 DSL 與 Authentication / Authorization |
| 進階框架功能 | 24-27 | 靜態檔案、Configuration、DI 容器、StatusPages |
| 整合實作與回顧 | 28-32 | TestKit 穩定化、用 Relix 做出完整的 Todo API、效能分析、回顧總結 |
這個系列是用 TDD 的方式寫的,不過不會走到 baby step 那麼細。如果每篇都照著「寫一個測試 → 讓它通過 → 重構 → 再寫下一個測試」一輪一輪推進,文章會變得非常長,真正想講的框架設計反而會被流程本身蓋過去
所以折衷了一下,每篇先把測試案例集中放在前面,讓你一次看完這個功能該有的行為和邊界條件,接著才是實作的程式碼
這只是文章的呈現順序,不是實際寫程式的順序。想自己練習的話,可以把前面那組測試每個都拆開拆細,一次只寫一個,先讓它失敗,再讓它通過,跑起來的感覺跟看文章不太一樣
還有一件事跟一般的 TDD 教學不太一樣,這個系列連測試工具本身都是長出來的,第 06 篇會做一個最陽春的 RelixTestKit,第 11 篇讓它支援 routing DSL,第 14 篇升級成 suspend,第 20 篇補上 JSON 驗證,第 28 篇再收斂成穩定的 relixTest { },所以你會看到測試寫法一路在變,那不是前後不一致,是工具跟著框架一起演進
還有一個跟呈現有關的取捨要先說,第 02 篇會用 Kotlin Toolchain CLI 的 jvm-cli template 開專案,它給的結構很扁平,實作全部放 src/,測試全部放 test/,沒有 package,也沒有 core / plugin / app 的 module 邊界,整個系列都維持這個樣子
這樣文章比較好講,每次提到「這段程式碼放哪個檔案」只要給檔名就夠了,不用一直帶著 com.example.relix.routing 這種前綴,你自己跟著做的時候也少一層目錄要對
不過實務上不會這樣放,至少會分 package,公開型別大多是一個檔案一個,core 跟 plugin 常常還會拆成不同的 module,讓相依的方向清楚,系列裡也有幾個檔案為了少開幾個新檔,一次放了好幾個型別,像第 22 篇的 Validation.kt 裡面有六個,第 23 篇的 Auth.kt 把 parser、marker interface、typed key 跟 plugin 本體全放在一起,那是為了文章說明的方便,實務上不建議你照著做
Kotlin 這邊,你會碰到幾個寫業務邏輯時不一定會用到、但做框架時特別好用的語法特性,Lambda with Receiver (RelixCall.() -> RelixResponse) 讓 API 寫起來像 DSL,typealias 和高階函式讓「流程」可以被組合,sealed class 用型別表達匹配結果,inline + reified 做出型別安全的 receive<T>(),協程 (Coroutine) 的部分,我們會先用同步跑完核心邏輯,等管線穩了再升級 suspend
每個特性都會在用到的那一篇拆開說明,不會只丟一段程式碼叫你照著用,對應的篇數先標出來
when 完整性檢查在第 08 篇suspend 如何轉換成 continuation 與狀態機在第 14 篇遇到不熟的語法不用急,對應文章會從用法一路講到背後原理
框架設計這邊,你會反覆面對「這東西該放在 HTTP 層還是框架層 ?」這個問題,抽象邊界畫對了,後面擴充就順,畫錯了,每次改都牽一髮動全身,另外,可測性是我們從頭到尾的堅持,不靠啟動真的 server,用 TestKit 就能跑完整流程
這個系列假設你已經會寫 Kotlin,不需要很深,但至少 val / var、if / when、null safety (?.、?:)、data class、基本的集合操作 (map、filter、fold)、以及 trailing lambda 語法,這些你應該都要會,如果你能看懂下面這段,就可以放心跟讀
data class User(val name: String, val age: Int)
fun List<User>.adults() = filter { it.age >= 18 }
如果上面那段程式碼看起來完全陌生,建議先從官方的 Kotlin Basics 開始,至少能寫出簡單的 class 和函式再回來
每次碰到新特性,我們會先用簡單的例子解釋它怎麼運作,確認你理解之後再套用到框架實作上,不會突然丟一個 reified 出來然後假裝你已經懂了
如果你只想快速上手 Ktor 趕專案進度,直接讀 Ktor 文件會比這個系列快得多,我們的重點是「理解框架怎麼做」,不是「教你用 Ktor」
如果你期待一個能直接上線的框架,也會失望,Relix 是教學用框架,刻意在某些地方做了簡化 (例如用 JDK HttpServer 而非 Netty),不打算假裝它能跟 Ktor 或 Spring Boot 比效能
反過來說,如果你是那種「框架用久了,想搞懂底層到底在幹嘛」的開發者,那你來對地方了
下一篇先把後面 33 篇都會沿用的專案骨架建起來,用 Kotlin Toolchain CLI 的 jvm-cli template 開一個乾淨的 Kotlin JVM 專案,確認 build、run、test 都跑得動,並補上一個 HttpServer 的環境測試
同步刊登於 Blog
圖片來源:AI 產生